import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;

import javax.swing.JOptionPane;
import javax.swing.text.Document;

import org.apache.http.HttpResponse;
import org.json.simple.JSONArray;

import com.dropbox.client.DropboxClient;
import com.dropbox.client.DropboxException;

public class SecurityHandler {
	
	private AES aes;
	private DropboxClient dbClient;
	private String root;
	private String path;
	private String backUpPath;
	private String userName;
	private Stylepad editor;
	
	private String logFilePath;
	
	private LogFileHandler localLogFile;
	private LogFileHandler serverLogFile;
	
	private SubFileHandler subFile;
	
	public SecurityHandler(DropboxClient client, String rootname, String pathname, String backUpPathName, Stylepad sEditor, String username){
		this.aes = new AES("I like cookies");
		
		this.localLogFile = new LogFileHandler();
		this.serverLogFile = new LogFileHandler();
		
		this.subFile = new SubFileHandler();
		
		this.dbClient = client;
		this.root = rootname;
		this.path = pathname;
		this.backUpPath = backUpPathName; 
		this.editor = sEditor;
		this.userName = username;
		
		//add log file path
	};
	
	public void fileCreateFolder(String folderName, String callback){
		try {
			dbClient.fileCreateFolder (root, folderName, callback);
		} catch (DropboxException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public Map fileDelete(String folder, String filename, String callback){
		try {
			if(folder != null && filename == null)
				return dbClient.fileDelete (root, folder, null);
				
			else {
				if (folder == null && filename != null){
					Map map = dbClient.fileDelete (root, path+"/"+aes.encrypt(filename), null);
										
					retrieveAndLoadServerLogFile ();
					
					SDAction sd = new SDAction();
					HashChain hc = new HashChain();
					
					float partNo;
					int seqNo;
					int ops = 2; //2 stands for delete
					byte[] HMAC = new byte[32]; //auto initialized with 0s
					byte[] latestHash = new byte[16];
					byte[] hash = new byte[16];
					
					//System.out.println(filename);
					partNo = sd.getSDNoFromFileName(filename);
					seqNo = localLogFile.getNextSeqNo(partNo);
					latestHash = localLogFile.getLatestHash(partNo, seqNo-1);
					hash = hc.generateHashChainValue(partNo, seqNo, userName, ops, HMAC, latestHash);
					
					//saving into LOCAL LOG FILE
					localLogFile.addEntry(partNo, seqNo, userName, ops, HMAC, hash);
					localLogFile.saveLogFile("local");
					
					//Add entry to SERVER LOG FILE
					serverLogFile.addEntry(partNo, seqNo, userName, ops, HMAC, hash);
					
					boolean serverUploadCheck = true;
					//if upload fail, keep retrying
					while(serverUploadCheck == false){
						if (!serverLogFile.saveLogFile("server"))
							serverUploadCheck = false;
						else
							serverUploadCheck = true;
						
						if(putFile(new File("server_log_file_L"),-1, null) == null){
							serverUploadCheck = false;
						}
						else{
							System.out.println("Server log file putfile passed");
							serverUploadCheck = true;
						}
					}					
					return map;

				}else
					return null;
			}
			
		} catch (DropboxException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		return null;
	}
	
	public void fileCopy(String toFolder, String fileToName, String fileFromName){
		try {
			dbClient.fileCopy (root, path + "/" + aes.encrypt(fileFromName), toFolder + "/" + aes.encrypt(fileToName), null);
		} catch (DropboxException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public Map fileMove(String fromFolder, String fileFromName, String fileToName){
		try {
			if (fromFolder != null)
				return dbClient.fileMove (root, fromFolder + "/" + aes.encrypt(fileFromName), path + "/" + aes.encrypt(fileToName), null);
			else
				return dbClient.fileMove (root, path + "/" + aes.encrypt(fileFromName), path + "/" + aes.encrypt(fileToName), null);
		} catch (DropboxException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		return null;
	}
	
	public Map metadata(String filename, boolean backup, int file_limit, String hash, boolean list, boolean status_in_response, String callback){
		
		Map<String, Object> meta = null;
		
		try {
			if (filename != null && backup == false)
				meta = dbClient.metadata (root, path + "/" + aes.encrypt(filename), file_limit, hash, list, status_in_response, callback);
			else {
				if(filename == null && backup == false)
					meta = dbClient.metadata (root, path, file_limit, hash, list, status_in_response, callback);
				else{
					if (filename == null && backup == true)
						meta = dbClient.metadata (root, backUpPath, file_limit, hash, list, status_in_response, callback);
					else {
						if (filename != null && backup == true)
							meta = dbClient.metadata (root, backUpPath + "/" + aes.encrypt(filename), file_limit, hash, list, status_in_response, callback);
						else
							return null;
					}
				}	
			}
		} catch (DropboxException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		}
		
		Object json_contents = meta.get("contents");
		
		if (json_contents != null && json_contents instanceof JSONArray) {
            Object entry;
            Iterator it = ((JSONArray)json_contents).iterator();
            
            while (it.hasNext()) {
                entry = it.next();
                if (entry instanceof Map) {
                	String path1 = (String)((Map) entry).get("path");

                	String[] dirs = path1.split("/"); // last element of dirs should be the file name

                	if(!path1.equals(path + "/" + "server_log_file_N")){
                		if(!path1.equals(path + "/" + "server_log_file_L")){                			
	                		String decryptedFileName = aes.decrypt(dirs[dirs.length - 1]);
	                	
		                	if (decryptedFileName == null){
		                		//print error message
		                		JOptionPane.showMessageDialog(editor.getFrame(),
		        	                    "File name of " + dirs[dirs.length - 1] + " could not be decrypted",
		        	                    "File Name Decryption Failed",
		        	                    JOptionPane.ERROR_MESSAGE);	
		                	}
	                	
		                	String decryptedpath = path + "/" + decryptedFileName;   
		            		((Map) entry).put("path", decryptedpath); 
                		}
                	}
                }
            }
		}
            
		meta.put("contents", json_contents);
		return meta;
	}

	public boolean checkServerLogFileStatus(){
		Map<String, Object> meta = null;
		
		//retrieve directory info
		try {
			meta = dbClient.metadata (root, path, 100, null, true, false, null);
		} catch (DropboxException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}		
		Object json_contents = meta.get("contents");
		
		if (json_contents != null && json_contents instanceof JSONArray) {
            Object entry;
            Iterator it = ((JSONArray)json_contents).iterator();
            
            while (it.hasNext()) {
                entry = it.next();
                if (entry instanceof Map) {
                	String path1 = (String)((Map) entry).get("path");

                	String[] dirs = path1.split("/"); // last element of dirs should be the file name

                	if(path1.equals(path + "/" + "server_log_file_L"))
                		return true;
                	if(path1.equals(path + "/" + "server_log_file_N"))
                		return false;
                }
            }
		}
            
		return false;
	}
	
	public HttpResponse getFile(String filename){
		try {
			return dbClient.getFile (root, path + "/" + aes.encrypt(filename));
			
		} catch (DropboxException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		}
	}
	
	public HttpResponse putFile(File file, int ops, byte[] HMAC){
		try {
			SDAction sd = new SDAction();
			HashChain hc = new HashChain();
			
			String fileName = file.getPath();
			
			//if ops == -1 means the file to be placed into the server is server LOG FILE. no need to add log entry
		    if(ops == -1){
				File file2 = new File(aes.encrypt(fileName));
				
				if(file2.exists()){
					file2.delete();
				}
				
				boolean success = file.renameTo(file2);
			    if (!success) {
			    	return null;
			    }				
		  
				HttpResponse resp = dbClient.putFile(root, path, file2);
				
				if(file2.exists()){
					file2.delete();
				}
				
				return resp;
		    }
			
		    else{
		    	//checks status of log files
		    	while(checkServerLogFileStatus()){
		    		try {
						this.wait(100);
					} catch (InterruptedException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
		    	}
		    	
				//lock log file
		    	fileMove(null, "server_log_file_N", "server_log_file_L");
		    	
		    	//retrieve server subfile
				retrieveAndLoadServerLogFile ();
	
				//push temp sub-file into server
				File file3 = new File(aes.encrypt(fileName));
				//File file3 = new File(aes.encrypt(fileName + "_temp"));
				
				if(file3.exists()){
					file3.delete();
				}
				
				boolean success = file.renameTo(file3);
			    if (!success) {
			    	return null;
			    }				
			    
				HttpResponse resp = dbClient.putFile(root, path, file3);
				
				if(file3.exists()){
					file3.delete();
				}
				
				//Add entry to LOCAL LOG FILE
				//initialise entry details				
				float partNo;
				int seqNo;
				byte[] latestHash = new byte[16];
				byte[] hash = new byte[16];
				byte[] hash2 = new byte[16];
				byte[] HMAC2 = new byte[32];
				
				partNo = sd.getSDNoFromFileName(fileName);				
				seqNo = localLogFile.getNextSeqNo(partNo);
				latestHash = localLogFile.getLatestHash(partNo, seqNo-1);
				hash = hc.generateHashChainValue(partNo, seqNo, userName, ops, HMAC, latestHash);
								
				localLogFile.addEntry(partNo, seqNo, userName, ops, HMAC, hash);
								
				localLogFile.saveLogFile("local");
								
				//Add entry to SERVER LOG FILE
				serverLogFile.addEntry(partNo, seqNo, userName, ops, HMAC, hash);
				
				boolean serverUploadCheck = true;
				
				//if upload fail, keep retrying
				do{
					if (!serverLogFile.saveLogFile("server"))
						serverUploadCheck = false;
					else
						serverUploadCheck = true;
					
					if(putFile(new File("server_log_file_L"),-1, null) == null){
						serverUploadCheck = false;
					}
					else{
						System.out.println("Server log file putfile passed");
						serverUploadCheck = true;
					}
				}while(serverUploadCheck == false);
				
				//rename sub-file to remove "_temp"
				//fileMove(null, fileName + "_temp", fileName);
				
				//unlock server log file
				fileMove(null, "server_log_file_L", "server_log_file_N");
				
				return resp;
			}
			
		} catch (DropboxException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		return null;
	}
	
	public boolean initiateLogFiles(ArrayList<String> sortedList){
		
		SDAction sd = new SDAction();
		
		//initialise entry details
		float partNo;
		int seqNo = 1;
		byte[] hash = new byte[16];
		byte[] HMAC = new byte[32];
		
		for(int a=0; a<16; a++){
			hash[a] = 0x00;
		}
		
		ArrayList<String> list1 = new ArrayList<String>();
		for (int i = 0; i < sortedList.size(); i++)
			list1.add(Float.toString(sd.getSDNoFromFileName(sortedList.get(i))));
		
		for(int j=0; j<sortedList.size(); j++){
			partNo = Float.parseFloat(list1.get(j));
			HMAC = extractHMACSubFile(sortedList.get(j));	
		
			localLogFile.addEntry(partNo, seqNo, userName, 1, HMAC, hash);
			localLogFile.saveLogFile("local");
				
			//Add entry to SERVER LOG FILE
			//lock server log file
			fileMove(null, "server_log_file_N", "server_log_file_L");
			
			serverLogFile.addEntry(partNo, seqNo, userName, 1, HMAC, hash);
			
			boolean serverUploadCheck = true;
			
			//if upload fail, keep retrying
			do{
				if (!serverLogFile.saveLogFile("server"))
					serverUploadCheck = false;
				else
					serverUploadCheck = true;
				
				if(putFile(new File("server_log_file_L"),-1, null) == null){
					//System.out.println("Server log file putfile fail");
					serverUploadCheck = false;
				}
				else{
					System.out.println("Server log file putfile passed");
					serverUploadCheck = true;
				}
			}while(serverUploadCheck == false);
			
			//unlock server log file
			fileMove(null, "server_log_file_L", "server_log_file_N");
		}
		
		return true;
	}
	
	public byte[] extractHMACSubFile(String fileName){
		
		//read in the first 64 characters of the file
		byte[] hash = new byte[32];
    	
		try {
			//retrieve from server
			HttpResponse resp = null;
			resp = getFile(fileName);
			
			if(resp == null){
				System.out.println("Cannot load file: " + fileName);
			}
			int status = resp.getStatusLine ().getStatusCode ();
			if(status != 200){
				System.out.println("Invalid status code for file: " + fileName);
			}
			
			Statistics.downloadRequest ++;
			System.out.println("DC##123 Add 1 to download request: "+Statistics.downloadRequest);
			Statistics.writeToLog("Download, "+Statistics.getCurrentTime()+", "+resp.getEntity().getContentLength()+" byte", "log.txt");
			
			System.out.println(fileName +" retrieved");
			
			InputStream in = resp.getEntity().getContent();	
			in.read(hash);
			return hash;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		}
	}

	public boolean additionalFileCheck(ArrayList<String> sortedList) {
		SDAction sd = new SDAction();
		
		// look up log file and generate a list of files that is supposed to be in the directory
		ArrayList<String> list1 = serverLogFile.generateFileNameListFromLogFile();
		if(list1 == null)
			initiateLogFiles(sortedList);
		list1 = serverLogFile.generateFileNameListFromLogFile();
		
		//sortedList contains the list of filenames in the directory. need to extract part numbers only
		ArrayList<String> list2 = new ArrayList<String>();

		for (int i = 0; i < sortedList.size(); i++)
			list2.add(Float.toString(sd.getSDNoFromFileName(sortedList.get(i))));
		
		//compare it with the current list
		if (list1.size() != list2.size()){
			//output error message
			System.out.println("Additional File Check Failed");
			return false;
		}
			
		for(int j = 0; j < list1.size(); j++){
			if(!list1.get(j).equals(list2.get(j))){
				//output error message
				System.out.println("list1: "+list1.get(j)+ "list2: " + list2.get(j));
				System.out.println("Additional File Check Failed");
				return false;
			}
		}
		
		System.out.println("Additional File Check Passed");
		return true;		
	}
	
	//The is the check for modification order verification
	public boolean globalInconsistencyCheck(ArrayList<String> sortedList){
		
		/*		
		1. The latest hi of all sub-files in both the server log file and the 
		local log file are equal
		*/
		SDAction sd = new SDAction();
		float partNo;
		int localSeqNo, serverSeqNo;
		byte[] serverLatestHash = new byte[16];
		byte[] localLatestHash = new byte[16];
		
		for (int i = 0; i < sortedList.size(); i++){
			//latest hash from server
			partNo = sd.getSDNoFromFileName(sortedList.get(i));		
			serverSeqNo = serverLogFile.getNextSeqNo(partNo);
			serverLatestHash = serverLogFile.getLatestHash(partNo, serverSeqNo-1);
			
			//latest hash from local log file
			localSeqNo = localLogFile.getNextSeqNo(partNo);
			localLatestHash = localLogFile.getLatestHash(partNo, localSeqNo-1);
			
			if(!Arrays.equals(serverLatestHash, localLatestHash)){
				System.out.println("Global Consistency Check 1 failed.");
				return false;
			}
		}
		
		System.out.println("Global Consistency Check 1 passed.");
		
		/*
		2. The latest hmaci of each sub-files stored in the local log file is 
		equal to the HMAC attached in each sub-files.
		*/
		
		byte[] logFileHMAC = new byte[32];
		byte[] subFileHMAC = new byte[32];
		
		for (int i = 0; i < sortedList.size(); i++){
			//latest HMAC in each sub-files
			HttpResponse resp = null;
			resp = getFile(sortedList.get(i));
			
			if(resp == null){
				System.out.println("Cannot load file: " + sortedList.get(i));
				return true;
			}
			int status = resp.getStatusLine ().getStatusCode ();
			if(status != 200){
				System.out.println("Invalid status code for file: " + sortedList.get(i));
				return true;
			}
			
			Statistics.downloadRequest ++;
			System.out.println("DC##123 Add 1 to download request: "+Statistics.downloadRequest);
			Statistics.writeToLog("Download, "+Statistics.getCurrentTime()+", "+resp.getEntity().getContentLength()+" byte", "log.txt");
			
			System.out.println(sortedList.get(i) + " retrieved");
			try {
				InputStream in = resp.getEntity().getContent();
				subFileHMAC = serverLogFile.extractHMAC(in);
				
			} catch (IllegalStateException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
				return true;
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
				return true;
			}
			
			//latest hmac from logfile
			partNo = sd.getSDNoFromFileName(sortedList.get(i));
			localSeqNo = localLogFile.getNextSeqNo(partNo);
			logFileHMAC = serverLogFile.getLatestHMAC(partNo, localSeqNo-1);
			
			//compare
			if (logFileHMAC == null || subFileHMAC == null)
				return true;
			
			StringBuffer sb1 = new StringBuffer();
		    for (int j = 0; j < logFileHMAC.length; j++) {
		    	sb1.append(Integer.toString((logFileHMAC[j] & 0xff) + 0x100, 16).substring(1));
		    }
		    
		    StringBuffer sb2 = new StringBuffer();
		    for (int k = 0; k < subFileHMAC.length; k++) {
		    	sb2.append(Integer.toString((subFileHMAC[k] & 0xff) + 0x100, 16).substring(1));
		    }
		    
			System.out.println("Global Consistency Check 2: HERE ARE THE HMACS: " + sb1.toString() + " & " + sb2.toString());
			
			if (!sb1.toString().equals(sb2.toString())){
				System.out.println("HMAC do not match. Global Consistency Check 2 failed.");
				return false;
			}
		}
		
		System.out.println("HMAC matches. Global Consistency Check 2 passed.");
		
		return true;
	}
	
	
	//This is the function tell calls all the security file checks
	public boolean filesValidationChecks(ArrayList<String> sortedList){
		
		//LOAD LOCAL LOG FILE
		if(!localLogFile.getLock()){
			if (!localLogFile.localLogFileHMACCheck()){
								
				//print some error
				JOptionPane.showMessageDialog(editor.getFrame(),
	                    "Local Log File HMAC Check failed",
	                    "Local Log File HMAC Check",
	                    JOptionPane.ERROR_MESSAGE);			
				
				return false;
			}		  
			System.out.println("local hmac check done");
			
			try {
			PipedInputStream in = new PipedInputStream();
			final PipedOutputStream out = new PipedOutputStream(in);
			
			new Thread(
				new Runnable(){
			    	public void run(){
			    		if(!localLogFile.decryptLocalLogFile(out)){
			  			
			  			//print some error
							JOptionPane.showMessageDialog(editor.getFrame(),
				                    "Local Log File Decryption failed",
				                    "Local Log File Decryption",
				                    JOptionPane.ERROR_MESSAGE);	
							
			    			System.out.println("local log file decryption done");
			  			return;
			    		}
			    		System.out.println("local log file decryption done");
			    	}
				}
			).start();
			
			if (!localLogFile.loadLocalLogFile(in)) 
				return false;
			
			in.close();
			out.close();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			System.out.println("local log file load done");
		}
		
		//load the serverlogfile!
		if(!retrieveAndLoadServerLogFile ())
			return false;

		
			//ADDITIONAL FILE CHECK
			if(!additionalFileCheck(sortedList)){
				//print some error
				JOptionPane.showMessageDialog(editor.getFrame(),
	                    "Additional File Check failed",
	                    "Additional File Check",
	                    JOptionPane.ERROR_MESSAGE);	
				//*NEED TO CHANGE TO FALSE
				return true;
			}
				
			//update local log file
			//call a function in log file handler, take the size of the two lists
			int updateResult = localLogFile.updateLocalLogFile(serverLogFile);
			if (updateResult == -1) {
				//print some error
				JOptionPane.showMessageDialog(editor.getFrame(),
	                    "Local Log File Update failed",
	                    "Local Log File Update",
	                    JOptionPane.ERROR_MESSAGE);	
				return false;
			}		
			
			//SAVE UPDATED LOCAL LOG FILE
			if (updateResult == 1) 
				localLogFile.saveLogFile("local");
		
		
		//global inconsistency check
		if(!globalInconsistencyCheck(sortedList)){
       		JOptionPane.showMessageDialog(editor.getFrame(),
                    "Global Inconsistency Check failed",
                    "Global Inconsistency Check",
                    JOptionPane.ERROR_MESSAGE);	
		}		
		
		return true;
		
	}
	
	public boolean retrieveAndLoadServerLogFile (){
		//RETRIEVE SERVER LOG FILE
		if(!serverLogFile.getLock()){
			HttpResponse resp = null;
			
			if(checkServerLogFileStatus())
				resp = getFile("server_log_file_L");
			else
				resp = getFile("server_log_file_N");
			
			if(resp == null){
				System.out.println("Cannot load file: " + "server_log_file");
				return false;
			}
			int status = resp.getStatusLine ().getStatusCode ();
			if(status != 200){
				System.out.println("Invalid status code for file: " + "server_log_file");
				return false;
			}
			
			Statistics.downloadRequest ++;
			System.out.println("DC##123 Add 1 to download request: "+Statistics.downloadRequest);
			Statistics.writeToLog("Download, "+Statistics.getCurrentTime()+", "+resp.getEntity().getContentLength()+" byte", "log.txt");
			
			System.out.println("server log file retrieved");
			
			//LOAD SERVER LOG FILE		
			//write response into a file			
			try {
				InputStream in = resp.getEntity().getContent();			
				OutputStream out = new FileOutputStream("server_log_file_L");
				
				int numRead = 0;
				byte[] buf = new byte[1024];
				
				while ((numRead = in.read(buf)) >= 0){
					out.write(buf, 0, numRead);
				}
				
				out.close();
				in.close();
	
				InputStream in2 = new FileInputStream("server_log_file_L");		
			
				if(!serverLogFile.serverLogFileHMACCheck(in2)){
					//print some error
					JOptionPane.showMessageDialog(editor.getFrame(),
		                    "Server Log File HMAC Check failed",
		                    "Server Log File HMAC Check",
		                    JOptionPane.ERROR_MESSAGE);	
					
					return false;
				}
				
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
			try {
				PipedInputStream in1 = new PipedInputStream();
				final PipedOutputStream out1 = new PipedOutputStream(in1);
				
				new Thread(
					new Runnable(){
				    	public void run(){
				    		if(!serverLogFile.decryptServerLogFile(out1)){			  			
				    			//print some error
								JOptionPane.showMessageDialog(editor.getFrame(),
					                    "Local Log File Decryption failed",
					                    "Local Log File Decryption",
					                    JOptionPane.ERROR_MESSAGE);	
				    			System.out.println("server log file decryption done");
				    			return;
				    		}
				    		System.out.println("server log file decryption done");
				    	}
					}
				).start();
				
				
				if (!serverLogFile.loadServerLogFile(in1)) 
					return false;

				in1.close();
				out1.close();
				
				File s_log_file = new File("server_log_file_L");
				s_log_file.delete();
			
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
			
			System.out.println("server log file load done");			
		}
		return true;
	}
}
